package com.google.code.gronono.gps.model;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.SystemUtils;
import org.apache.log4j.Logger;

import com.google.code.gronono.commons.i18n.BundleKey;
import com.google.code.gronono.commons.i18n.BundleName;
import com.google.code.gronono.commons.observable.AbstractObservable;
import com.google.code.gronono.gps.model.enums.FilterDateType;
import com.google.code.gronono.gps.model.enums.FilterType;
import com.google.code.gronono.gps.model.enums.PatternType;
import com.google.code.gronono.gps.model.enums.ProcessAction;

/**
 * Contient la configuration du programme.
 * <br/>Cette classe implémente le design pattern singleton.
 *
 * @author Arnaud BRUNET
 */
@BundleName("com.google.code.gronono.gps.gps")
public class Configuration extends AbstractObservable {
	/** Logger */
	private static final Logger logger = Logger.getLogger(Configuration.class);

	/** Instance du singleton. */
	private static final Configuration configuration = new Configuration();

	/** Fichier de configuration par défaut. */
	private static final String DEFAULT_CONF_FILE = "com/google/code/gronono/gps/conf.properties";

	/** Nom du fichier de configuration. */
	@BundleKey("configuration.file.name")
	private static String CONFIGURATION_FILE_NAME;
	/** Pattern de la date du jour. */
	@BundleKey("configuration.today.pattern")
	private static String TODAY_PATTERN;
	/** Message de chargement de la configuration. */
	@BundleKey("configuration.load.from.file")
	private static String CONFIGURATION_LOAD_FROM_FILE;
	/** Message de chargement de la configuration par défaut. */
	@BundleKey("configuration.load.from.default.file")
	private static String CONFIGURATION_LOAD_FROM_DEFAULT_FILE;

	/** Clé du dossier source dans le fichier de configuration. */
	private static final String SRC_DIR_PROPERTY = "src.dir";
	/** Clé du dossier de destination dans le fichier de configuration. */
	private static final String DST_DIR_PROPERTY = "dst.dir";

	/** Clé de l'action dans le fichier de configuration. */
	private static final String PROCESS_ACTION_PROPERTY = "process.action";

	/** Clé du flag d'écrasement dans le fichier de configuration. */
	private static final String OVERWRITE_PROPERTY = "overwrite";
	/** Clé du flag de récursivité dans le fichier de configuration. */
	private static final String RECURSIVE_PROPERTY = "recursive";
	/** Clé du flag de rotation dans le fichier de configuration. */
	private static final String ROTATE_PROPERTY = "rotate";

	/** Clé filtre des fichiers dans le fichier de configuration. */
	private static final String FILTER_TYPE_PROPERTY = "filter.type";
	/** Clé filtre des extensions de fichiers dans le fichier de configuration. */
	private static final String FILTER_EXTENSION_PROPERTY = "filter.extension";
	/** Clé filtre des noms de fichiers dans le fichier de configuration. */
	private static final String FILTER_EXPRESSION_PROPERTY = "filter.expression";

	/** Clé d'affichage des rejets du filtre dans le fichier de configuration. */
	private static final String FILTER_SHOW_REJECTS_PROPERTY = "filter.show.rejects";

	/** Clé du flag de filtre par date dans le fichier de configuration. */
	private static final String FILTER_BY_DATE_PROPERTY = "filter.by.date";
	/** Clé du format de date pour le filtre dans le fichier de configuration. */
	private static final String FILTER_DATE_FORMAT_PROPERTY = "filter.date.format";
	/** Clé du type de date pour le filtre dans le fichier de configuration. */
	private static final String FILTER_DATE_TYPE_PROPERTY = "filter.date.type";
	/** Clé de la date min pour le filtre dans le fichier de configuration. */
	private static final String FILTER_DATE_MIN_PROPERTY = "filter.date.min";
	/** Clé de la date max pour le filtre dans le fichier de configuration. */
	private static final String FILTER_DATE_MAX_PROPERTY = "filter.date.max";

	/** Clé du type de pattern dans le fichier de configuration. */
	private static final String PATTERN_TYPE_PROPERTY = "pattern.type";
	/** Clé de l'expression de pattern JS dans le fichier de configuration. */
	private static final String PATTERN_JS_EXPRESSION_PROPERTY = "pattern.js.expression";
	/** Clé de l'expression de pattern dans le fichier de configuration. */
	private static final String PATTERN_KEYWORDS_EXPRESSION_PROPERTY = "pattern.keywords.expression";

	/** Clé du flag de traitement des rejets dans le fichier de configuration. */
	private static final String REJECT_PROCESS_PROPERTY = "reject.process";
	/** Clé du dossier cible pour les rejets dans le fichier de configuration. */
	private static final String REJECT_DIR_PROPERTY = "reject.dir";
	/** Clé du pattern pour les rejets dans le fichier de configuration. */
	private static final String REJECT_PATTERN_KEYWORDS_EXPRESSION_PROPERTY = "reject.pattern.keywords.expression";

	/** Fichier de configuration courant. */
	private File userConfFile;

	/** Les propriétés lues dans le fichier. */
	private Properties properties;

	/**
	 * Charge la configuration.<br/>
	 * Lit le fichier ${user.dir}/gps.properties.
	 * Si le fichier n'existe pas, lit le fichier {@link #DEFAULT_CONF_FILE}.
	 *
	 * @param reset flag permettant de forcer une réinitialisation à partir des valeurs par défaut.
	 * @return La configuration lue.
	 *
	 * @throws IOException En cas d'erreur lors de la lecture.
	 */
	public static Configuration load(final boolean reset) throws IOException {
		final StringBuilder confFilePath = new StringBuilder(SystemUtils.USER_DIR).append(File.separator).append(CONFIGURATION_FILE_NAME);
		configuration.userConfFile = new File(confFilePath.toString());
		if (!reset && configuration.userConfFile.exists()) {
			logger.info(MessageFormat.format(CONFIGURATION_LOAD_FROM_FILE, configuration.userConfFile.getAbsolutePath()));
			final InputStream is = new FileInputStream(configuration.userConfFile);
			load(configuration, is);
		} else {
			logger.info(CONFIGURATION_LOAD_FROM_DEFAULT_FILE);
			final InputStream is = Configuration.class.getClassLoader().getResourceAsStream(DEFAULT_CONF_FILE);
			load(configuration, is);
		}
		return configuration;
	}

	/**
	 * Lit la configuration à partir de l'{@link InputStream} spécifié dans l'objet {@link Configuration} spécifié.
	 *
	 * @param configuration La configuration résultante.
	 * @param is Le flux à lire.
	 *
	 * @throws IOException En cas d'erreur.
	 */
	private static void load(final Configuration configuration, final InputStream is) throws IOException {
		configuration.properties = new Properties();
		try {
			configuration.properties.load(is);
		} finally {
			is.close();
		}
	}

	/**
	 * Constructeur par défaut.
	 */
	private Configuration() {
		// Ne fait rien, et c'est bien comme ça
	}

	/**
	 * Enregistre la configuration dans le fichier utilisateur.
	 *
	 * @throws IOException En cas d'erreur lors de la sauvegarde.
	 */
	public void save() throws IOException {
		logger.info("Sauvegarde des options dans " + userConfFile.getAbsolutePath());
		final OutputStream os = new FileOutputStream(userConfFile);
		try {
			properties.store(os, null);
		} finally {
			os.close();
		}
	}

	/**
	 * Supprime le fichier de configuration utilisateur.
	 * <br/>Rmq: méthode inutilisée.
	 */
	public void delete() {
		logger.info("Suppression du fichier de configuration " + userConfFile.getAbsolutePath());
		FileUtils.deleteQuietly(userConfFile);
	}

	/**
	 * Donne le dossier source.
	 * @return Le dossier source.
	 */
	public File getSrcDir() {
		final String property = properties.getProperty(SRC_DIR_PROPERTY);
		final File dir = getDirectory(property);
		if (dir != null) {
			return dir;
		}
		return new File(property);
	}

	/**
	 * Change le dossier source.
	 * @param directory Le nouveau dossier source.
	 */
	public void setSrcDir(String directory) {
		if (directory == null) {
			directory = "";
		}
		final String oldValue = (String) properties.setProperty(SRC_DIR_PROPERTY, directory);
		firePropertyChange(SRC_DIR_PROPERTY, oldValue, directory);
	}

	/**
	 * Donne le dossier de destination.
	 * @return Le dossier de destination.
	 */
	public File getDstDir() {
		final String property = properties.getProperty(DST_DIR_PROPERTY);
		final File dir = getDirectory(property);
		if (dir != null) {
			return dir;
		}
		return new File(property);
	}

	/**
	 * Change le dossier de destination.
	 * @param directory Le nouveau dossier de destination.
	 */
	public void setDstDir(String directory) {
		if (directory == null) {
			directory = "";
		}
		final String oldValue = (String) properties.setProperty(DST_DIR_PROPERTY, directory);
		firePropertyChange(DST_DIR_PROPERTY, oldValue, directory);
	}

	/**
	 * Donne l'action à effectuer.
	 * @return L'action à effectuer.
	 */
	public ProcessAction getProcessAction() {
		final String action = properties.getProperty(PROCESS_ACTION_PROPERTY);
		if (StringUtils.isNotBlank(action)) {
			return ProcessAction.valueOf(action.toUpperCase());
		}
		return ProcessAction.MOVE;
	}

	/**
	 * Change l'action à effectuer.
	 * @param action La nouvelle action.
	 */
	public void setProcessAction(ProcessAction action) {
		if (action == null) {
			action = ProcessAction.MOVE;
		}
		final ProcessAction oldValue = getProcessAction();
		properties.setProperty(PROCESS_ACTION_PROPERTY, action.toString());
		firePropertyChange(PROCESS_ACTION_PROPERTY, oldValue, action);
	}

	/**
	 * Donne le flag d'écrasement.
	 * @return Le flag d'écrasement.
	 */
	public boolean isOverwrite() {
		return Boolean.parseBoolean(properties.getProperty(OVERWRITE_PROPERTY));
	}

	/**
	 * Change le flag d'écrasement.
	 * @param overwrite Le nouveau flag d'écrasement.
	 */
	public void setOverwrite(final boolean overwrite) {
		final String oldValue = (String) properties.setProperty(OVERWRITE_PROPERTY, String.valueOf(overwrite));
		firePropertyChange(OVERWRITE_PROPERTY, oldValue, String.valueOf(overwrite));
	}

	/**
	 * Donne le flag de récursivité.
	 * @return Le flag de récursivité.
	 */
	public boolean isRecursive() {
		return Boolean.parseBoolean(properties.getProperty(RECURSIVE_PROPERTY));
	}

	/**
	 * Change le flag de récursivité.
	 * @param recursive Le nouveau flag de récursivité.
	 */
	public void setRecursive(final boolean recursive) {
		final String oldValue = (String) properties.setProperty(RECURSIVE_PROPERTY, String.valueOf(recursive));
		firePropertyChange(RECURSIVE_PROPERTY, oldValue, String.valueOf(recursive));
	}

	/**
	 * Donne le flag de rotation.
	 * @return Le flag de rotation.
	 */
	public boolean isRotate() {
		return Boolean.parseBoolean(properties.getProperty(ROTATE_PROPERTY));
	}

	/**
	 * Change le flag de rotation.
	 * @param rotate Le nouveau flag de rotation.
	 */
	public void setRotate(final boolean rotate) {
		final String oldValue = (String) properties.setProperty(ROTATE_PROPERTY, String.valueOf(rotate));
		firePropertyChange(ROTATE_PROPERTY, oldValue, String.valueOf(rotate));
	}

	/**
	 * Donne le type de filtre à appliquer.
	 * @return le type de filtre à appliquer.
	 */
	public FilterType getFilterType() {
		final String filterType = properties.getProperty(FILTER_TYPE_PROPERTY);
		if (StringUtils.isNotBlank(filterType)) {
			return FilterType.valueOf(filterType.toUpperCase());
		}
		return FilterType.EXTENSION;
	}

	/**
	 * Change le type de filtre à appliquer.
	 * @param filterType Le nouveau type de filtre à appliquer.
	 */
	public void setFilterType(FilterType filterType) {
		if (filterType == null) {
			filterType = FilterType.EXTENSION;
		}
		final FilterType oldValue = getFilterType();
		properties.setProperty(FILTER_TYPE_PROPERTY, filterType.toString());
		firePropertyChange(FILTER_TYPE_PROPERTY, oldValue, filterType);
	}

	/**
	 * Donne l'extension (ou la liste d'extensions) permettant de filtrer les fichiers.
	 * @return L'extension (ou la liste d'extensions) du filtre.
	 */
	public String getFilterExtension() {
		return properties.getProperty(FILTER_EXTENSION_PROPERTY);
	}

	/**
	 * Change l'extension (ou la liste d'extensions) du filtre.
	 * @param extension La nouvelle extension (ou la liste d'extensions) du filtre.
	 */
	public void setFilterExtension(String extension) {
		if (extension == null) {
			extension = "";
		}
		final String oldValue = (String) properties.setProperty(FILTER_EXTENSION_PROPERTY, extension);
		firePropertyChange(FILTER_EXTENSION_PROPERTY, oldValue, extension);
	}

	/**
	 * Donne l'expression régulière permettant de filtrer les fichiers.
	 * @return L'expression régulière du filtre.
	 */
	public String getFilterExpression() {
		return properties.getProperty(FILTER_EXPRESSION_PROPERTY);
	}

	/**
	 * Change l'expression du filtre.
	 * @param expression La nouvelle expression du filtre.
	 */
	public void setFilterExpression(String expression) {
		if (expression == null) {
			expression = "";
		}
		final String oldValue = (String) properties.setProperty(FILTER_EXPRESSION_PROPERTY, expression);
		firePropertyChange(FILTER_EXPRESSION_PROPERTY, oldValue, expression);
	}

	/**
	 * Donne le flag d'affichage des rejets du filtre.
	 * @return Le flag d'affichage des rejets du filtre.
	 */
	public boolean isFilterShowRejects() {
		return Boolean.parseBoolean(properties.getProperty(FILTER_SHOW_REJECTS_PROPERTY));
	}

	/**
	 * Change le flag d'affichage des rejets du filtre.
	 * @param filterShowRejects Le nouveau flag d'affichage des rejets du filtre.
	 */
	public void setFilterShowRejects(final boolean filterShowRejects) {
		final String oldValue = (String) properties.setProperty(FILTER_SHOW_REJECTS_PROPERTY, String.valueOf(filterShowRejects));
		firePropertyChange(FILTER_SHOW_REJECTS_PROPERTY, oldValue, String.valueOf(filterShowRejects));
	}

	/**
	 * Donne le flag de filtrage par dates.
	 * @return Le flag de filtrage par dates.
	 */
	public boolean isFilterByDate() {
		return Boolean.parseBoolean(properties.getProperty(FILTER_BY_DATE_PROPERTY));
	}

	/**
	 * Change le flag de filtrage par dates.
	 * @param filterByDate Le nouveau flag de filtrage par dates.
	 */
	public void setFilterByDate(final boolean filterByDate) {
		final String oldValue = (String) properties.setProperty(FILTER_BY_DATE_PROPERTY, String.valueOf(filterByDate));
		firePropertyChange(FILTER_BY_DATE_PROPERTY, oldValue, String.valueOf(filterByDate));
	}

	/**
	 * Donne le type de filtre de date à appliquer.
	 * @return le type de filtre de date à appliquer.
	 */
	public FilterDateType getFilterDateType() {
		final String filterDateType = properties.getProperty(FILTER_DATE_TYPE_PROPERTY);
		if (StringUtils.isNotBlank(filterDateType)) {
			return FilterDateType.valueOf(filterDateType.toUpperCase());
		}
		return FilterDateType.EXIF;
	}

	/**
	 * Change le type de filtre de date à appliquer.
	 * @param filterDateType Le nouveau type de filtre de date à appliquer.
	 */
	public void setFilterDateType(FilterDateType filterDateType) {
		if (filterDateType == null) {
			filterDateType = FilterDateType.EXIF;
		}
		final FilterDateType oldValue = getFilterDateType();
		properties.setProperty(FILTER_DATE_TYPE_PROPERTY, filterDateType.toString());
		firePropertyChange(FILTER_DATE_TYPE_PROPERTY, oldValue, filterDateType);
	}

	/**
	 * Donne le format de date pour le filtre par dates.
	 * @return le format de date pour le filtre par dates.
	 */
	public String getFilterDateFormat() {
		return properties.getProperty(FILTER_DATE_FORMAT_PROPERTY);
	}

	/**
	 * Change le format de date pour le filtre par dates.
	 * @param filterDateFormat le format de date pour le filtre par dates.
	 */
	public void setFilterDateFormat(String filterDateFormat) {
		if (filterDateFormat == null) {
			filterDateFormat = "";
		}
		final String oldValue = (String) properties.setProperty(FILTER_DATE_FORMAT_PROPERTY, filterDateFormat);
		firePropertyChange(FILTER_DATE_FORMAT_PROPERTY, oldValue, filterDateFormat);
	}

	/**
	 * Donne la date min pour le filtre par dates.
	 * @return la date min pour le filtre par dates.
	 */
	public String getFilterDateMin() {
		final String property = properties.getProperty(FILTER_DATE_MIN_PROPERTY);
		return getFormattedDate(property);
	}

	/**
	 * Change la date min pour le filtre par dates.
	 * @param filterDateMin la date min pour le filtre par dates.
	 */
	public void setFilterDateMin(String filterDateMin) {
		if (filterDateMin == null) {
			filterDateMin = "";
		}
		final String oldValue = (String) properties.setProperty(FILTER_DATE_MIN_PROPERTY, filterDateMin);
		firePropertyChange(FILTER_DATE_MIN_PROPERTY, oldValue, filterDateMin);
	}

	/**
	 * Donne la date max pour le filtre par dates.
	 * @return la date max pour le filtre par dates.
	 */
	public String getFilterDateMax() {
		final String property = properties.getProperty(FILTER_DATE_MAX_PROPERTY);
		return getFormattedDate(property);
	}

	/**
	 * Change la date max pour le filtre par dates.
	 * @param filterDateMax la date max pour le filtre par dates.
	 */
	public void setFilterDateMax(String filterDateMax) {
		if (filterDateMax == null) {
			filterDateMax = "";
		}
		final String oldValue = (String) properties.setProperty(FILTER_DATE_MAX_PROPERTY, filterDateMax);
		firePropertyChange(FILTER_DATE_MAX_PROPERTY, oldValue, filterDateMax);
	}

	/**
	 * Donne le type de pattern à appliquer.
	 * @return le type de pattern à appliquer.
	 */
	public PatternType getPatternType() {
		final String patternType = properties.getProperty(PATTERN_TYPE_PROPERTY);
		if (StringUtils.isNotBlank(patternType)) {
			return PatternType.valueOf(patternType.toUpperCase());
		}
		return PatternType.KEYWORDS;
	}

	/**
	 * Change l'action à effectuer.
	 * @param patternType La nouvelle action.
	 */
	public void setPatternType(PatternType patternType) {
		if (patternType == null) {
			patternType = PatternType.KEYWORDS;
		}
		final PatternType oldValue = getPatternType();
		properties.setProperty(PATTERN_TYPE_PROPERTY, patternType.toString());
		firePropertyChange(PATTERN_TYPE_PROPERTY, oldValue, patternType);
	}

	/**
	 * Donne l'expression pattern du traitement par mots-clé.
	 * @return L'expression pattern du traitement par mots-clé.
	 */
	public String getPatternKeywordsExpression() {
		return properties.getProperty(PATTERN_KEYWORDS_EXPRESSION_PROPERTY);
	}

	/**
	 * Change l'expression de pattern du traitement par mots-clé.
	 * @param expression La nouvelle expression.
	 */
	public void setPatternKeywordsExpression(String expression) {
		if (expression == null) {
			expression = "";
		}
		final String oldValue = (String) properties.setProperty(PATTERN_KEYWORDS_EXPRESSION_PROPERTY, expression);
		firePropertyChange(PATTERN_KEYWORDS_EXPRESSION_PROPERTY, oldValue, expression);
	}

	/**
	 * Donne l'expression de pattern JS du traitement.
	 * @return L'expression de pattern JS du traitement.
	 */
	public String getPatternJsExpression() {
		return properties.getProperty(PATTERN_JS_EXPRESSION_PROPERTY);
	}

	/**
	 * Change l'expression de pattern JS du traitement.
	 * @param expression La nouvelle expression.
	 */
	public void setPatternJsExpression(String expression) {
		if (expression == null) {
			expression = "";
		}
		final String oldValue = (String) properties.setProperty(PATTERN_JS_EXPRESSION_PROPERTY, expression);
		firePropertyChange(PATTERN_JS_EXPRESSION_PROPERTY, oldValue, expression);
	}

	/**
	 * Donne le flag de traitement des rejets.
	 * @return Le flag de traitement des rejets.
	 */
	public boolean isRejectProcess() {
		return Boolean.parseBoolean(properties.getProperty(REJECT_PROCESS_PROPERTY));
	}

	/**
	 * Change le flag de traitement des rejets.
	 * @param rejectProcess Le nouveau flag de traitement des rejets.
	 */
	public void setRejectProcess(final boolean rejectProcess) {
		final String oldValue = (String) properties.setProperty(REJECT_PROCESS_PROPERTY, String.valueOf(rejectProcess));
		firePropertyChange(REJECT_PROCESS_PROPERTY, oldValue, String.valueOf(rejectProcess));
	}

	/**
	 * Donne le dossier de rejet.
	 * @return Le dossier de rejet.
	 */
	public File getRejectDir() {
		final String property = properties.getProperty(REJECT_DIR_PROPERTY);
		final File dir = getDirectory(property);
		if (dir != null) {
			return dir;
		}
		return new File(property);
	}

	/**
	 * Change le dossier de rejet.
	 * @param directory Le nouveau dossier de rejet.
	 */
	public void setRejectDir(String directory) {
		if (directory == null) {
			directory = "";
		}
		final String oldValue = (String) properties.setProperty(REJECT_DIR_PROPERTY, directory);
		firePropertyChange(REJECT_DIR_PROPERTY, oldValue, directory);
	}

	/**
	 * Donne l'expression de pattern de traitement des rejets.
	 * @return L'expression de pattern de traitement des rejets.
	 */
	public String getRejectPatternKeywordsExpression() {
		return properties.getProperty(REJECT_PATTERN_KEYWORDS_EXPRESSION_PROPERTY);
	}

	/**
	 * Change l'expression de pattern de traitement des rejets.
	 * @param expression La nouvelle expression.
	 */
	public void setRejectPatternKeywordsExpression(String expression) {
		if (expression == null) {
			expression = "";
		}
		final String oldValue = (String) properties.setProperty(REJECT_PATTERN_KEYWORDS_EXPRESSION_PROPERTY, expression);
		firePropertyChange(REJECT_PATTERN_KEYWORDS_EXPRESSION_PROPERTY, oldValue, expression);
	}

	/**
	 * Permet de récupérer la date sous le bon format.
	 * <br/>Gère un mot-clé pour la date du jour : {@link #TODAY_PATTERN}.
	 * @param property La propriété à lire.
	 * @return La date sous le bon format.
	 */
	private String getFormattedDate(String property) {
		if (TODAY_PATTERN.equalsIgnoreCase(property)) {
			final String dateFormat = getFilterDateFormat();
			if (StringUtils.isNotBlank(dateFormat)) {
				final DateFormat df = new SimpleDateFormat(dateFormat);
				property = df.format(new Date());
			}
		}
		return property;
	}

	/**
	 * Permet de traiter la récupération des répertoires.
	 * <br/>Tente d'interpréter une propriété système avec {@link System#getProperty(String)}.
	 * <br/>Retourne le {@link File} correspondant à la <code>property</code> spécifiée s'il s'agit d'un répertoire valide, <code>null</code> sinon.
	 * @param property La propriété.
	 * @return Le répertoire s'il est valide, <code>null</code> sinon.
	 */
	private File getDirectory(final String property) {
		// Gère certaines propriétés système de la jre
		if (System.getProperty(property) != null) {
			final File file = new File(System.getProperty(property));
			if (file.exists() && file.isDirectory()) return file.getAbsoluteFile();
		} else {
			final File file = new File(property);
			if (file.exists() && file.isDirectory()) return file.getAbsoluteFile();
		}
		return null;
	}

}
